A proposta consiste em analisar e prevero preço das estadias Airbnb na cidade do Rio de Janeiro através do agrupamento de caracaterísticas físicas e de localização dos imóveis utilizando K-means e posteriormente para os clusters gerados aplicam-se técnicas de séries temporais (aqui aplicadas média móvel e suaviazação exponencial simples) e medidas através dos erro MAPE - Erro absoluto médio percentual para verificar a acurácia da previsão. Não foi objeto de análise verificar as informações do host bem com scores de pontuação do host ou da localidad visto que podem ser consideradas aspectos mais subjetivos de comportamento humano, necessitando de outros tipos de análise (como análise de sentimentos).
In [41]:
#Importação dos necessários
import pandas as pd
import numpy as np
import math
import matplotlib.pyplot as plt
import seaborn as sns
import matplotlib.style as style
from sklearn.preprocessing import MinMaxScaler
from sklearn.model_selection import train_test_split
from sklearn.cluster import KMeans
from datetime import datetime, timedelta
from statsmodels.tsa.stattools import adfuller
from statsmodels.tsa.arima_model import ARIMA
import warnings
%matplotlib inline
warnings.filterwarnings("ignore")
pd.set_option('display.max_columns', 100)
In [2]:
# importar o arquivo listings.csv para um DataFrame
listings=pd.read_csv("http://data.insideairbnb.com/brazil/rj/rio-de-janeiro/2021-01-26/data/listings.csv.gz")
calendar=pd.read_csv("http://data.insideairbnb.com/brazil/rj/rio-de-janeiro/2021-01-26/data/calendar.csv.gz")
In [3]:
listings.head(2)
Out[3]:
id listing_url scrape_id last_scraped name description neighborhood_overview picture_url host_id host_url ... review_scores_communication review_scores_location review_scores_value license instant_bookable calculated_host_listings_count calculated_host_listings_count_entire_homes calculated_host_listings_count_private_rooms calculated_host_listings_count_shared_rooms reviews_per_month
0 17878 https://www.airbnb.com/rooms/17878 20210126045954 2021-01-27 Very Nice 2Br in Copacabana w. balcony, fast WiFi Discounts for long term stays. <br />- Large b... This is the one of the bests spots in Rio. Bec... https://a0.muscache.com/pictures/65320518/3069... 68997 https://www.airbnb.com/users/show/68997 ... 10.0 10.0 9.0 NaN t 1 1 0 0 2.01
1 25026 https://www.airbnb.com/rooms/25026 20210126045954 2021-01-27 Beautiful Modern Decorated Studio in Copa Our apartment is a little gem, everyone loves ... Copacabana is a lively neighborhood and the ap... https://a0.muscache.com/pictures/3003965/68ebb... 3746246 https://www.airbnb.com/users/show/3746246 ... 10.0 10.0 9.0 NaN f 11 11 0 0 1.84

2 rows × 74 columns

In [4]:
calendar.head(2)
Out[4]:
listing_id date available price adjusted_price minimum_nights maximum_nights
0 6213104 2021-01-27 t $244.00 $244.00 1.0 1125.0
1 6213104 2021-01-28 t $244.00 $244.00 1.0 1125.0
Removendo colunas não interessantes à análise. Resumidamente, foram desconsiradas aqui informações sobre o host, disponibilidade do imóvel, os links de acesso ao site Airbnb e detalhes sobre avaliações do imóvel e do host.
In [5]:
listings.drop(columns=['listing_url','scrape_id','last_scraped','name','description','neighborhood_overview', 'picture_url',
'host_id','host_url','host_name','host_since','host_location','host_about','host_response_time','host_response_rate',
'host_acceptance_rate','host_thumbnail_url','host_picture_url','host_neighbourhood','host_listings_count',
'host_total_listings_count','host_verifications','host_has_profile_pic','host_identity_verified','neighbourhood','amenities',
'minimum_minimum_nights','maximum_minimum_nights','minimum_maximum_nights','neighbourhood_group_cleansed',
'maximum_maximum_nights','minimum_nights_avg_ntm','maximum_nights_avg_ntm','calendar_updated','has_availability',
'availability_30','availability_60','availability_90','availability_365','calendar_last_scraped','number_of_reviews',
'number_of_reviews_ltm','number_of_reviews_l30d','first_review','last_review','review_scores_accuracy',
'review_scores_cleanliness','review_scores_checkin','review_scores_communication','review_scores_location',
'review_scores_value','license','instant_bookable','calculated_host_listings_count','calculated_host_listings_count_entire_homes',
'calculated_host_listings_count_private_rooms','calculated_host_listings_count_shared_rooms'],inplace=True)
In [6]:
calendar['available'] = calendar.available.map(lambda x: 1 if x == 't' else 0)
calendar.date = pd.to_datetime(calendar.date)
calendar['price'] = calendar['price'].str.replace('$', '').str.replace(',', '')
calendar['price'] = calendar['price'].astype(float)
calendar['adjusted_price'] = calendar['adjusted_price'].str.replace('$', '').str.replace(',', '')
calendar['adjusted_price'] = calendar['adjusted_price'].astype(float)
In [7]:
calendar.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 9529874 entries, 0 to 9529873
Data columns (total 7 columns):
 #   Column          Dtype         
---  ------          -----         
 0   listing_id      int64         
 1   date            datetime64[ns]
 2   available       int64         
 3   price           float64       
 4   adjusted_price  float64       
 5   minimum_nights  float64       
 6   maximum_nights  float64       
dtypes: datetime64[ns](1), float64(4), int64(2)
memory usage: 509.0 MB
In [30]:
calendar.describe()
Out[30]:
listing_id available price adjusted_price minimum_nights maximum_nights
count 9.529874e+06 9529874.0 9.529681e+06 9.529681e+06 9.528347e+06 9.528347e+06
mean 2.481193e+07 0.0 1.010015e+03 1.008949e+03 4.835935e+00 3.904977e+04
std 1.551999e+07 0.0 9.894297e+03 9.892788e+03 2.038694e+01 6.189122e+06
min 1.787800e+04 0.0 0.000000e+00 0.000000e+00 1.000000e+00 1.000000e+00
25% 1.206936e+07 0.0 1.600000e+02 1.600000e+02 1.000000e+00 9.000000e+01
50% 2.304177e+07 0.0 2.950000e+02 2.950000e+02 2.000000e+00 1.125000e+03
75% 4.056471e+07 0.0 5.920000e+02 5.910000e+02 4.000000e+00 1.125000e+03
max 4.789324e+07 0.0 2.187712e+06 2.187712e+06 1.111000e+03 1.000000e+09
In [34]:
# box-plot das principais variáveis
calendar.price.plot(kind='box', vert=False, figsize=(15, 3));
plt.show()
calendar.adjusted_price.plot(kind='box', vert=False, figsize=(15, 3));
plt.show()
calendar.minimum_nights.plot(kind='box', vert=False, figsize=(15, 3));
plt.show()
calendar.maximum_nights.plot(kind='box', vert=False, figsize=(15, 3));
plt.show()
In [36]:
#Podemos observar que adjusted_price é identica à price e portando podemos desconsiderar esta informação
calendar.drop(columns=['adjusted_price'],inplace=True)
In [37]:
#Fracionando o dataframe a partir dos valores de preço
x1=len(calendar[calendar['price']<100000])
x2=len(calendar[calendar['price']<75000])
x3=len(calendar[calendar['price']<50000])
x4=len(calendar[calendar['price']<25000])
x5=len(calendar[calendar['price']<10000])
x6=len(calendar[calendar['price']<5000])
x7=len(calendar[calendar['price']<2500])
x8=len(calendar[calendar['price']<1000])
x9=len(calendar[calendar['price']<500])
x10=len(calendar[calendar['price']<200])

print(round(x1/len(calendar),4),round(x2/len(calendar),4),round(x3/len(calendar),4),round(x4/len(calendar),4),
      round(x5/len(calendar),4),round(x6/len(calendar),4),round(x7/len(calendar),4),round(x8/len(calendar),4),
      round(x9/len(calendar),4),round(x10/len(calendar),4))
0.9974 0.9974 0.9969 0.996 0.9926 0.9831 0.9599 0.8574 0.6917 0.3248
Podemos observar que abaixo de 2500 podemos reduzir o dataframe para 95,99% do tamanho original. Ainda mantém-se outliers (95,99-75 ~ 22%) que podem não ser outliers (a depender da relação de outros atributos com o preço) vide exemplo "https://www.airbnb.com.br/rooms/17717006?adults=2&check_in=2021-07-12&check_out=2021-07-18&federated_search_id=f508bb21-aa8e-4bdf-9e74-d63907edb479&source_impression_id=p3_1614992529_VgnWDwRQLVXqbbyZ&guests=1".
In [8]:
df=calendar[calendar['price']<=2500]
In [41]:
# box-plot das principais variáveis
df.price.plot(kind='box', vert=False, figsize=(15, 3));
plt.show()
df.minimum_nights.plot(kind='box', vert=False, figsize=(15, 3));
plt.show()
df.maximum_nights.plot(kind='box', vert=False, figsize=(15, 3));
plt.show()
In [9]:
df['dia'] = df['date'].dt.day
df['semana'] = df['date'].dt.week
df['mes'] = df['date'].dt.month
df['dia_ano'] = df['date'].dt.dayofyear
df['dia_semana'] = df['date'].dt.dayofweek
C:\Users\tarci\anaconda3\lib\site-packages\ipykernel_launcher.py:1: SettingWithCopyWarning: 
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
  """Entry point for launching an IPython kernel.
C:\Users\tarci\anaconda3\lib\site-packages\ipykernel_launcher.py:2: SettingWithCopyWarning: 
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
  
C:\Users\tarci\anaconda3\lib\site-packages\ipykernel_launcher.py:3: SettingWithCopyWarning: 
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
  This is separate from the ipykernel package so we can avoid doing imports until
C:\Users\tarci\anaconda3\lib\site-packages\ipykernel_launcher.py:4: SettingWithCopyWarning: 
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
  after removing the cwd from sys.path.
C:\Users\tarci\anaconda3\lib\site-packages\ipykernel_launcher.py:5: SettingWithCopyWarning: 
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
  """
In [89]:
plt.plot(df['price'].groupby(df['dia']).mean())
plt.title("Preço médio por dia do mês")
Out[89]:
Text(0.5, 1.0, 'Preço médio por dia do mês')
In [90]:
plt.plot(df['price'].groupby(df['dia_semana']).mean())
plt.title("Preço médio por dia da semana")
Out[90]:
Text(0.5, 1.0, 'Preço médio por dia da semana')
In [86]:
plt.plot(df['price'].groupby(df['dia_ano']).mean())
plt.title("Preço médio por dia do ano")
Out[86]:
Text(0.5, 1.0, 'Preço médio por dia do ano')
In [57]:
plt.plot(df['price'].groupby(df['mes']).mean())
plt.title("Preço médio por mês")
Out[57]:
Text(0.5, 1.0, 'Preço médio por mês')
In [58]:
plt.plot(df['price'].groupby(df['semana']).mean())
plt.title("Preço médio por semana do ano")
Out[58]:
Text(0.5, 1.0, 'Preço médio por semana')
Podemos observar que o preço é influenciado pelo período do tempo. No geral, os preços são maiores no início do ano (especialmente fevereiro e com picos em Julho, Setembro e Dezembro/Janeiro), entre os dias 10 a 18 do mês e no final de semana (sexta e sábado).
In [92]:
plt.scatter(df['price'],df['minimum_nights'])
plt.title("Relacionamento entre preço e número mínimo de noites")
plt.show()
plt.scatter(df['price'],df['maximum_nights'])
plt.title("Relacionamento entre preço e número máximo de noites")
plt.show()
Podemos observar que não existe uma relação entre mínimo/máximo de estadia com o preço e portanto desconsiderados na análise do preço. Até o momento, a variável tempo (subdividida em dia da semana, dia do mês, dia do ano, mês e semana) foi a que mostrou padrão de relacionamento com a variável preço. Agora faremos a análise com o dataset listings com os atributos do imóvel.
In [99]:
df['price'].hist(bins=15, figsize=(10,5))
plt.title("Histograma variável preço")
Out[99]:
Text(0.5, 1.0, 'Histograma variável preço')
In [101]:
df['price'].describe()
Out[101]:
count    9.175259e+06
mean     4.342591e+02
std      4.325488e+02
min      0.000000e+00
25%      1.560000e+02
50%      2.800000e+02
75%      5.100000e+02
max      2.500000e+03
Name: price, dtype: float64
75% dos preços concentram-se em até $510 e,em média, a diária no Rio de Janeiro é de $280.
In [11]:
#Removendo colunas com dados faltantes e minimo/máximo diárias conforme desconsiderado anteriormente.
listings.drop(columns=['bathrooms','review_scores_rating','reviews_per_month','minimum_nights','maximum_nights'],inplace=True)
In [12]:
listings['price'] = listings['price'].str.replace('$', '').str.replace(',', '')
listings['price'] = listings['price'].astype(float)
In [13]:
#Filtro de preço de 2500 conforme realizado no dataset de calendário.
df2=listings[listings['price']<=2500]
In [14]:
df2['bathrooms_text']=df2['bathrooms_text'].str.split(" ",n=2,expand=True)[0]
C:\Users\tarci\anaconda3\lib\site-packages\ipykernel_launcher.py:1: SettingWithCopyWarning: 
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
  """Entry point for launching an IPython kernel.
In [15]:
df2.bathrooms_text=df2.bathrooms_text.str.replace("Shared",'0')
df2.bathrooms_text=df2.bathrooms_text.str.replace("Half-bath",'0')
df2.bathrooms_text=df2.bathrooms_text.str.replace("Private",'1')
df2.dropna(inplace=True)
C:\Users\tarci\anaconda3\lib\site-packages\pandas\core\generic.py:5303: SettingWithCopyWarning: 
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
  self[name] = value
C:\Users\tarci\anaconda3\lib\site-packages\ipykernel_launcher.py:4: SettingWithCopyWarning: 
A value is trying to be set on a copy of a slice from a DataFrame

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
  after removing the cwd from sys.path.
In [16]:
df2.bathrooms_text=df2.bathrooms_text.astype(float)
In [17]:
#Categorizado as variáveis do tipo texto
from sklearn.preprocessing import LabelEncoder
labelencoder = LabelEncoder()
df2['cat_bairro']=labelencoder.fit_transform(df2['neighbourhood_cleansed'])
df2['cat_patrimonio']=labelencoder.fit_transform(df2['property_type'])
df2['cat_imovel']=labelencoder.fit_transform(df2['room_type'])
C:\Users\tarci\anaconda3\lib\site-packages\ipykernel_launcher.py:4: SettingWithCopyWarning: 
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
  after removing the cwd from sys.path.
C:\Users\tarci\anaconda3\lib\site-packages\ipykernel_launcher.py:5: SettingWithCopyWarning: 
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
  """
C:\Users\tarci\anaconda3\lib\site-packages\ipykernel_launcher.py:6: SettingWithCopyWarning: 
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
  
In [175]:
plt.figure (figsize=(30, 20))
plt.plot(df2['price'].groupby(df2['neighbourhood_cleansed']).mean())
plt.xticks(rotation=90)
plt.rcParams['xtick.labelsize'] = 5
plt.rcParams['ytick.labelsize'] = 10
plt.title("Preço médio por bairro")
Out[175]:
Text(0.5, 1.0, 'Preço médio por bairro')
In [180]:
df2['price'].groupby(df2['neighbourhood_cleansed']).mean().sort_values()
Out[180]:
neighbourhood_cleansed
Vila Kosmos              39.000000
Coelho Neto              81.250000
Galeão                   84.000000
Senador Vasconcelos      84.500000
Jardim Carioca           93.333333
                          ...     
Cacuia                  795.000000
Anchieta                800.000000
Vaz Lobo                822.000000
Cavalcanti              925.000000
Vista Alegre           1304.000000
Name: price, Length: 150, dtype: float64
In [182]:
plt.figure (figsize=(30, 20))
plt.plot(df2['price'].groupby(df2['property_type']).mean())
plt.xticks(rotation=90)
plt.rcParams['xtick.labelsize'] = 20
plt.rcParams['ytick.labelsize'] =20
plt.title("Preço médio por patrimônio")
Out[182]:
Text(0.5, 1.0, 'Preço médio por patrimônio')
In [183]:
df2['price'].groupby(df2['property_type']).mean().sort_values()
Out[183]:
property_type
Private room in boat                52.0
Private room in tent                54.0
Shared room in casa particular      56.0
Shared room in townhouse            60.0
Shared room in chalet               62.0
                                   ...  
Boat                              1000.0
Casa particular                   1010.8
Private room in castle            1200.0
Houseboat                         1280.0
Entire vacation home              2500.0
Name: price, Length: 84, dtype: float64
In [186]:
plt.figure (figsize=(30, 20))
plt.plot(df2['price'].groupby(df2['room_type']).mean())
plt.xticks(rotation=90)
plt.rcParams['xtick.labelsize'] = 20
plt.rcParams['ytick.labelsize'] =20
plt.title("Preço médio por tipo de imóvel")
Out[186]:
Text(0.5, 1.0, 'Preço médio por tipo de imóvel')
In [187]:
df2['price'].groupby(df2['room_type']).mean().sort_values()
Out[187]:
room_type
Shared room        204.331070
Private room       252.125801
Hotel room         267.356322
Entire home/apt    528.563806
Name: price, dtype: float64
A visualização acima nos permite verificar que há relação de preço com o bairro, tipo de propriedade e com o tipo de imóvel (aqui mais evidente que entire home/apt possui maior preço comparado aos demais - com preços próximos)
In [191]:
# criar uma matriz de correlação
corr = df2[['price', 'accommodates', 'bathrooms_text', 'bedrooms']].corr()
# plotar um heatmap a partir das correlações
sns.heatmap(corr, cmap='RdBu', fmt='.2f', square=True, linecolor='white', annot=True);
Observamos também correlação do preço com a característica de acomodações, quantida de banheiros e quartos. Com as características a serem consideradas para o modelo, podemos uní-las em um único dataframe.
In [18]:
df2.rename(columns={'id':'listing_id'},inplace=True)
C:\Users\tarci\anaconda3\lib\site-packages\pandas\core\frame.py:4133: SettingWithCopyWarning: 
A value is trying to be set on a copy of a slice from a DataFrame

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
  errors=errors,
In [19]:
df_clean=pd.merge(df2,df,on=['listing_id'],how='inner')
In [20]:
df_clean.drop(columns=['date','available','minimum_nights','maximum_nights','price_y','listing_id','host_is_superhost'],inplace=True)
df_clean.rename(columns={'price_x':'price'},inplace=True)
In [21]:
from sklearn.preprocessing import StandardScaler
In [22]:
features = ['accommodates','bathrooms_text', 'bedrooms', 'cat_bairro', 'cat_patrimonio',
       'cat_imovel','dia','mes','semana','dia_ano','dia_semana']
# Separating out the features
x = df_clean.loc[:, features].values
# Separating out the target
y = df_clean.loc[:,['price']].values
# Standardizing the features
x = StandardScaler().fit_transform(x)
In [23]:
# Função que inicializa um número k de clusters
def score(df_norm, k):
    kmeans_k = KMeans(k)
    model_k = kmeans_k.fit(df_norm)
    return abs(model_k.score(df_norm))
In [24]:
df_norm=pd.DataFrame(x)
df_norm.columns=['accommodates','bathrooms_text', 'bedrooms', 'cat_bairro', 'cat_patrimonio',
       'cat_imovel','dia','mes','semana','dia_ano','dia_semana']
In [334]:
# Initliazes a line space with range from 1 to 20
scores = []
centers = np.linspace(1,20,20)
for i in range(1, 21):
    scores.append(score(df_norm[['accommodates','bathrooms_text', 'bedrooms', 'cat_bairro', 'cat_patrimonio',
       'cat_imovel']], i))
In [335]:
# Investigates the change within-cluster distance across number of clusters
fig, ax = plt.subplots()
ax.plot(centers, scores)
ax.set(ylabel='Distancia média entre clusters', xlabel='número de clusters')
ax.grid()
plt.xticks(np.arange(2,20,2))
plt.title("Distância x número de clusters atributos físicos do imóvel")
plt.show()
In [25]:
# Re-fits the k-means model with the selected number of clusters
kmeans_k = KMeans(8)
model_k = kmeans_k.fit(df_norm[['accommodates','bathrooms_text', 'bedrooms', 'cat_bairro', 'cat_patrimonio',
       'cat_imovel']])
df_norm_kmeans = model_k.predict(df_norm[['accommodates','bathrooms_text', 'bedrooms', 'cat_bairro', 'cat_patrimonio',
       'cat_imovel']])
In [26]:
# Creates a new column showing clusters in the dataset used in the model
df_norm['Clusters'] = pd.Series(df_norm_kmeans, index=df_norm.index)
df_norm['price']=df_clean['price']
In [27]:
df_norm.head()
Out[27]:
accommodates bathrooms_text bedrooms cat_bairro cat_patrimonio cat_imovel dia mes semana dia_ano dia_semana Clusters price
0 0.371907 -0.669171 0.370928 -0.588235 -0.376648 -0.634493 1.282072 -1.602246 -1.491855 -1.480200 -0.498828 6 201.0
1 0.371907 -0.669171 0.370928 -0.588235 -0.376648 -0.634493 1.395760 -1.602246 -1.491855 -1.470705 0.001622 6 201.0
2 0.371907 -0.669171 0.370928 -0.588235 -0.376648 -0.634493 1.509447 -1.602246 -1.491855 -1.461210 0.502072 6 201.0
3 0.371907 -0.669171 0.370928 -0.588235 -0.376648 -0.634493 1.623134 -1.602246 -1.491855 -1.451715 1.002522 6 201.0
4 0.371907 -0.669171 0.370928 -0.588235 -0.376648 -0.634493 1.736821 -1.602246 -1.491855 -1.442220 1.502972 6 201.0
In [28]:
# Groups data by clusters and shows the means of numerical values by cluster
df_norm_new = df_norm.groupby(['Clusters']).mean()
df_norm_new.head(10)
Out[28]:
accommodates bathrooms_text bedrooms cat_bairro cat_patrimonio cat_imovel dia mes semana dia_ano dia_semana price
Clusters
0 -0.057797 0.007080 -0.624059 0.295167 3.203364 2.268505 0.000032 -0.000411 -0.000201 -0.000407 0.000546 234.480521
1 0.156229 0.121648 0.256150 0.966268 -0.560261 -0.634493 -0.000135 0.000297 0.000304 0.000287 0.000163 558.944803
2 -0.328699 -0.591476 -0.619078 -0.457341 -0.552453 -0.634493 0.000103 -0.000625 -0.000505 -0.000609 -0.000007 311.463010
3 -0.813414 -0.402055 -0.605086 -0.405613 1.087683 1.429862 -0.000092 0.000677 0.000884 0.000671 0.000090 247.897761
4 3.253372 3.984041 3.669641 0.201041 0.106930 -0.313603 0.000467 -0.000440 -0.001555 -0.000456 -0.001468 1219.765700
5 -0.752986 -0.367634 -0.568778 1.835486 1.272928 1.454507 -0.000141 0.000790 0.001037 0.000781 0.000454 238.847798
6 0.488275 0.259150 0.548015 -0.800316 -0.601190 -0.631875 -0.000061 -0.000042 -0.000196 -0.000048 -0.000048 565.034833
7 1.437845 1.346564 1.533184 -0.029185 -0.546272 -0.591854 0.000219 -0.000399 -0.000944 -0.000398 -0.000584 873.488397
In [29]:
# Displays average prices by cluster
avg_price_cluster = df_norm_new['price'].sort_values(ascending=False).head(15)
avg_price_cluster.plot.bar(figsize=(10,4))
Out[29]:
<matplotlib.axes._subplots.AxesSubplot at 0x260239066c8>
Previsão por séries temporais. definição do erro MAPE, média móvel (mm) e suavização exponencial (ses).
In [62]:
def mape(real,prev):
    return np.mean(np.abs((real - prev) /real)) * 100
In [136]:
def media_movel(series):
    plt.figure(figsize=(20,10))
    for w in range(2,11):
        rolling_mean = series.rolling(window=w).mean()
        erro = mape(series[w:], rolling_mean[w:])
        plt.title("Média móvel com janelas de tempo")
        plt.plot(rolling_mean, label="Previsão de janela {} com MAPE = {}".format(w,round(erro,2)))
    plt.plot(series,'b',label='Real')
    plt.legend()
    plt.grid(True)
    plt.show()
In [178]:
def suavizacao_exponencial(series):
    plt.figure(figsize=(20,10))
    for alpha in range(1,10):
        result = [series[0]]
        for n in range(1, len(series)):
            result.append((alpha/10) * series[n] + (1 - (alpha/10)) * result[n-1])
        erro = mape(series,result)
        plt.plot(result, label="Previsão de alfa {} com MAPE = {}".format((alpha/10),round(erro,2)))
    plt.title("Suavização exponencial")
    plt.plot(series,'b',label='Real')
    plt.legend()
    plt.grid(True)
    plt.show()
In [179]:
def suavizacao_exponencial_dupla(series, alpha, beta):
    result = [series[0]]
    for n in range(1, len(series)+1):
        if n == 1:
            level, trend = series[0], series[1] - series[0]
        if n >= len(series):
            value = result[-1]
        else:
            value = series[n]
        last_level, level = level, alpha*value + (1-alpha)*(level+trend)
        trend = beta*(level-last_level) + (1-beta)*trend
        result.append(level+trend)
    
    erro = mape(series,result[1:])
    plt.title("Suavização exponencial dupla")
    plt.plot(series,'b',label='Real')
    plt.plot(result, label="Previsão alfa = {} e beta={} com MAPE = {}".format(alpha,beta,round(erro,2)))
    plt.legend()
    plt.grid(True)
Calculando as previsões para Dia+1 e respectivos erros MAPE
In [31]:
df_clean['clusters']=df_norm['Clusters']
In [32]:
df_clean2=pd.DataFrame(df_clean['price'].groupby([df_clean['clusters'],df_clean['dia_ano']]).mean())
df_clean2=df_clean2.reset_index()
In [33]:
for i in range(0,8):
    plt.figure (figsize=(40, 10))
    plt.rcParams.update({'font.size': 40})
    ex=df_clean2['dia_ano'][df_clean2['clusters']==i]
    ey=df_clean2['price'][df_clean2['clusters']==i]
    plt.plot(ex,ey)
    plt.title('Série temporal do preço no cluster {}'.format(i))
    plt.show()
In [34]:
df_clean3=pd.DataFrame(df_clean['price'].groupby([df_clean['room_type'],df_clean['dia_ano']]).mean())
In [37]:
df_clean3=df_clean3.reset_index()
In [38]:
plt.figure (figsize=(40, 10))
plt.rcParams.update({'font.size': 40})
ex=df_clean3['dia_ano'][df_clean3['room_type']=='Entire home/apt']
ey=df_clean3['price'][df_clean3['room_type']=='Entire home/apt']
plt.plot(ex,ey)
plt.title('Série temporal do preço Entire home/apt')
plt.show()

plt.figure (figsize=(40, 10))
plt.rcParams.update({'font.size': 40})
ex=df_clean3['dia_ano'][df_clean3['room_type']=='Hotel room']
ey=df_clean3['price'][df_clean3['room_type']=='Hotel room']
plt.plot(ex,ey)
plt.title('Série temporal do preço Hotel room')
plt.show()

plt.figure (figsize=(40, 10))
plt.rcParams.update({'font.size': 40})
ex=df_clean3['dia_ano'][df_clean3['room_type']=='Private room']
ey=df_clean3['price'][df_clean3['room_type']=='Private room']
plt.plot(ex,ey)
plt.title('Série temporal do preço Private room')
plt.show()

plt.figure (figsize=(40, 10))
plt.rcParams.update({'font.size': 40})
ex=df_clean3['dia_ano'][df_clean3['room_type']=='Shared room']
ey=df_clean3['price'][df_clean3['room_type']=='Shared room']
plt.plot(ex,ey)
plt.title('Série temporal do preço Shared room')
plt.show()
Pode ser claramente observado o efeito de inicio da pandemia no mês de Fevereiro no Brasil com a queda brusca de locações e consequentemente do preço (lei oferta/demanda) na maioria dos clusters e também nos tipos de locação e, apesar de no gráfico mostrar uma queda acentuada no preço, podemos constatar que o intervalo de variação do preço é pequeno (vide valores eixo vertical de cada gráfico). Abaixo verifica-se a estacionaridade de cada série a partir do teste adfuller que, para valores abaixo de 0.05 a série é estacionária e portando testes de previsão podem ser aplicados na série e, caso contrário, é feita uma diferenciação para tornar a série estacionária (se for possível). Sendo a série não estacionária, novas diferenciações podem ser feitas mas a perda de informação se torna maior. Neste projeto, optou-se por realizar somente 1 diferenciação e, em caso negativo de estacionaridade, a série é desconsiderada.
In [45]:
adf=adfuller(df_clean3[df_clean3['room_type']=='Entire home/apt']['price'])
if adf[1]<=0.05:
    print("Entire home/apt: estacionaria")
else:
    adf=adfuller(df_clean3[df_clean3['room_type']=='Entire home/apt']['price'].diff().fillna(0))
    if adf[1]<=0.05:
        df_clean3[df_clean3['room_type']=='Entire home/apt']['price']=(
        df_clean3[df_clean3['room_type']=='Entire home/apt']['price'].diff().fillna(0))
        print("Entire home/apt: estacionaria com diferenciacao")
    else:
        print('Entire home/apt: nao estacionaria')
    
    
adf=adfuller(df_clean3[df_clean3['room_type']=='Hotel room']['price'])
if adf[1]<=0.05:
    print("Hotel room: estacionaria")
else:
    adf=adfuller(df_clean3[df_clean3['room_type']=='Hotel room']['price'].diff().fillna(0))
    if adf[1]<=0.05:
        df_clean3[df_clean3['room_type']=='Hotel room']['price']=(
        df_clean3[df_clean3['room_type']=='Hotel room']['price'].diff().fillna(0))
        print("Hotel room: estacionaria com diferenciacao")
    else:
        print('Hotel room: nao estacionaria')
    
adf=adfuller(df_clean3[df_clean3['room_type']=='Private room']['price'])
if adf[1]<=0.05:
    print("Private room: estacionaria")
else:
    adf=adfuller(df_clean3[df_clean3['room_type']=='Private room']['price'].diff().fillna(0))
    if adf[1]<=0.05:
        df_clean3[df_clean3['room_type']=='Private room']['price']=(
        df_clean3[df_clean3['room_type']=='Private room']['price'].diff().fillna(0))
        print("Private room: estacionaria com diferenciacao")
    else:
        print('Private room: nao estacionaria')
    
adf=adfuller(df_clean3[df_clean3['room_type']=='Shared room']['price'])
if adf[1]<=0.05:
    print("Shared room: estacionaria")
else:
    adf=adfuller(df_clean3[df_clean3['room_type']=='Shared room']['price'].diff().fillna(0))
    if adf[1]<=0.05:
        df_clean3[df_clean3['room_type']=='Shared room']['price']=(
        df_clean3[df_clean3['room_type']=='Shared room']['price'].diff().fillna(0))
        print("Shared room: estacionaria com diferenciacao")
    else:
        print('Shared room: nao estacionaria')

for i in range(0,8):
    adf=adfuller(df_clean2[df_clean2['clusters']==i]['price'])
    if adf[1]<=0.05:
        print("Cluster {}: estacionaria".format(i))
    else:
        adf=adfuller(df_clean2[df_clean2['clusters']==i]['price'].diff().fillna(0))
        if adf[1]<=0.05:
            df_clean2[df_clean2['clusters']==i]['price']=(
            df_clean2[df_clean2['clusters']==i]['price'].diff().fillna(0))
            print("Cluster {}: estacionaria com diferenciacao".format(i))
        else:
            print("Cluster {}: nao estacionaria".format(i))
Entire home/apt: estacionaria com diferenciacao
Hotel room: estacionaria com diferenciacao
Private room: estacionaria com diferenciacao
Shared room: estacionaria com diferenciacao
Cluster 0: estacionaria com diferenciacao
Cluster 1: estacionaria com diferenciacao
Cluster 2: estacionaria com diferenciacao
Cluster 3: estacionaria
Cluster 4: estacionaria com diferenciacao
Cluster 5: estacionaria
Cluster 6: estacionaria com diferenciacao
Cluster 7: estacionaria com diferenciacao
In [192]:
home_apt=df_clean3[df_clean3['room_type']=='Entire home/apt']['price']
In [205]:
hotel_room=df_clean3[df_clean3['room_type']=='Hotel room']['price'].reset_index()
hotel_room.drop(columns=['index'],inplace=True)
hotel_room=hotel_room.iloc[:,0]
In [190]:
private_room=df_clean3[df_clean3['room_type']=='Private room']['price'].reset_index()
private_room.drop(columns=['index'],inplace=True)
private_room=private_room.iloc[:,0]
In [193]:
shared_room=df_clean3[df_clean3['room_type']=='Shared room']['price'].reset_index()
shared_room.drop(columns=['index'],inplace=True)
shared_room=shared_room.iloc[:,0]
In [194]:
cluster_0=df_clean2[df_clean2['clusters']==0]['price'].reset_index()
cluster_0.drop(columns=['index'],inplace=True)
cluster_0=cluster_0.iloc[:,0]
In [211]:
cluster_1=df_clean2[df_clean2['clusters']==1]['price'].reset_index()
cluster_1.drop(columns=['index'],inplace=True)
cluster_1=cluster_1.iloc[:,0]
In [196]:
cluster_2=df_clean2[df_clean2['clusters']==2]['price'].reset_index()
cluster_2.drop(columns=['index'],inplace=True)
cluster_2=cluster_2.iloc[:,0]
In [197]:
cluster_3=df_clean2[df_clean2['clusters']==3]['price'].reset_index()
cluster_3.drop(columns=['index'],inplace=True)
cluster_3=cluster_3.iloc[:,0]
In [198]:
cluster_4=df_clean2[df_clean2['clusters']==4]['price'].reset_index()
cluster_4.drop(columns=['index'],inplace=True)
cluster_4=cluster_4.iloc[:,0]
In [199]:
cluster_5=df_clean2[df_clean2['clusters']==5]['price'].reset_index()
cluster_5.drop(columns=['index'],inplace=True)
cluster_5=cluster_5.iloc[:,0]
In [200]:
cluster_6=df_clean2[df_clean2['clusters']==6]['price'].reset_index()
cluster_6.drop(columns=['index'],inplace=True)
cluster_6=cluster_6.iloc[:,0]
In [201]:
cluster_7=df_clean2[df_clean2['clusters']==7]['price'].reset_index()
cluster_7.drop(columns=['index'],inplace=True)
cluster_7=cluster_7.iloc[:,0]
In [202]:
media_movel(home_apt)
suavizacao_exponencial(home_apt)
for j in range(1,10):
    plt.figure(figsize=(20,10))
    for i in range(1,10):
        suavizacao_exponencial_dupla(home_apt,j/10,i/10)
    plt.show()
In [207]:
media_movel(hotel_room)
suavizacao_exponencial(hotel_room)
for j in range(1,10):
    plt.figure(figsize=(20,10))
    for i in range(1,10):
        suavizacao_exponencial_dupla(hotel_room,j/10,i/10)
    plt.show()
hotel room: 0.03 (ses 0.9)
In [208]:
media_movel(cluster_0)
suavizacao_exponencial(cluster_0)
for j in range(1,10):
    plt.figure(figsize=(20,10))
    for i in range(1,10):
        suavizacao_exponencial_dupla(cluster_0,j/10,i/10)
    plt.show()
In [212]:
media_movel(cluster_1)
suavizacao_exponencial(cluster_1)
for j in range(1,10):
    plt.figure(figsize=(20,10))
    for i in range(1,10):
        suavizacao_exponencial_dupla(cluster_1,j/10,i/10)
    plt.show()
In [213]:
media_movel(cluster_2)
suavizacao_exponencial(cluster_2)
for j in range(1,10):
    plt.figure(figsize=(20,10))
    for i in range(1,10):
        suavizacao_exponencial_dupla(cluster_2,j/10,i/10)
    plt.show()
In [214]:
media_movel(cluster_3)
suavizacao_exponencial(cluster_3)
for j in range(1,10):
    plt.figure(figsize=(20,10))
    for i in range(1,10):
        suavizacao_exponencial_dupla(cluster_3,j/10,i/10)
    plt.show()
In [215]:
media_movel(cluster_4)
suavizacao_exponencial(cluster_4)
for j in range(1,10):
    plt.figure(figsize=(20,10))
    for i in range(1,10):
        suavizacao_exponencial_dupla(cluster_4,j/10,i/10)
    plt.show()
In [ ]:
 
In [216]:
media_movel(cluster_5)
suavizacao_exponencial(cluster_5)
for j in range(1,10):
    plt.figure(figsize=(20,10))
    for i in range(1,10):
        suavizacao_exponencial_dupla(cluster_5,j/10,i/10)
    plt.show()
In [217]:
media_movel(cluster_6)
suavizacao_exponencial(cluster_6)
for j in range(1,10):
    plt.figure(figsize=(20,10))
    for i in range(1,10):
        suavizacao_exponencial_dupla(cluster_6,j/10,i/10)
    plt.show()
In [ ]:
 
In [218]:
media_movel(cluster_7)
suavizacao_exponencial(cluster_7)
for j in range(1,10):
    plt.figure(figsize=(20,10))
    for i in range(1,10):
        suavizacao_exponencial_dupla(cluster_7,j/10,i/10)
    plt.show()
Conclusão: Pode ser observado que as medidas de erro MAPE foram baixas para os grupos de imóvel já disponibilizados nas bases de dados. O método de duavização simples foi o que apresentou o melhor resultado com MAPE de 0.01. Na clusterização foram obtidos os mesmos índices de erro MAPE porém, no total de erros, a clusterização se mostrou melhor com erros próximos a 0.1 em todos os métodos considerados enquando no agrupamento por imóveis este erro ficou próximo de 0.4. A clusterização se mostra promissora para resultados de previsão de preços do Airbnb visto que há uma gama elevada de atributos do imóvel e do host que podem ser consideradas para aumentar a aproximação dos modelos à relidade do problema.